查看原文
其他

口碑 App 各 Bundle 之间的依赖分析指南

莫川 mPaaS 2019-05-29

背景


口碑的 O2O 业务 Bundle,目前需要在支付宝和口碑独客这两个 App 中的运行。目前口碑 App 也是使用 mPaaS 框架,一些基础服务比如 ConfigService,H5 容器,RPC 网络库,AntUI 库,Sync,扫码,Push 等,和支付宝保持一致,并对于不兼容的地方进行拉分支单独改造,对于支持多 App 的 Bundle,直接使用支付宝的基线。


那么,每次业务在支付宝上发版之后,同步到口碑 App 时,都需要将口碑 App 的基线进行升级。所谓基线升级,就是将支付宝中对应的 Bundle 版本号同步到独客,将有定制分支的 Bundle 进行代码 merge。支付宝 App 有几百个 Bundle,而口碑 App 的 Bundle 规模也已达到相似规模。


这几百个 Bundle 中,其中几十个 Bundle 是口碑 App 有分支定制以及特有的,剩下的 Bundle 直接从支付宝已有体系内进行索引。


为了减小包大小,我们需要确定这些 Bundle 之间的依赖关系,更确切一点,我们想知道这些 Bundle 的依赖程度:如果删除某个 Bundle,将会对剩余的哪些 Bundle 有影响?有哪些 Bundle 可以直接删除?解决这些问题,我们需要分析这几百多个 Bundle 的依赖关系。


1

Bundle 依赖分析方法

几百个 Bundle,靠人工一个个看,是梳理不过来的。而且,每个版本都有代码更新,依赖关系都有可能变化。因此,需要我们开发对应的工具进行分析。

方案 1:分析 build.gradle

我们知道,在 Android 开发中,如果我们需要依赖某个 Jar 包,我们会在 module 的 build.gradle 中添加,比如:

  1. dependencies {

  2.        provided 'com.alipay.android.phone.thirdparty:fastjson-api:1.1.45@jar'

  3.        provided 'com.alipay.android.phone.thirdparty:wire-api:1.5.3@jar'

  4.        ...

  5. }


对于某个 Bundle,我们可以通过分析 Bundle 中的 module,然后解析 build.gradle 文件,获取 Bundle 之间的依赖。



  • 【结论】
    这种方案出现的问题:

  1. dependencies 的依赖可能有冗余,会有多余的 dependencies 出现,会影响结果的准确性;

  2. 这种方案我们只能在知道 Bundle 之间的依赖关系,并不知道依赖了其中的多少个类?有哪些地方依赖?

方案 2:分析每个 Bundle 的每个 Java 文件的 import 区域

为了解决方案 1 中的问题,我们使用方案 2 进行依赖分析,就是分析每个 Bundle 的每个 Java 文件的 import 区域,然后建立它们之间的映射关系。

以 o2ocommon 这个 Bundle 为例,bundle、module、class 和 import 的区域关系如下:

然后,我们将 Bundle 之间的依赖,转换为分析 Java 文件之间的依赖;并且能够计算出 Bundle 之间有多少个类依赖,以及依赖了多少次。

  • 结论
    方案 2 通过


2

方案实施:JDA 依赖分析工具开发

  • 3.1 拉取每个 Bundle 对应的源码
    通过脚本,将 Bundle 对应的 gitlab 代码库拉取到的本地,切换到需要的分支。

  • 3.2 根据 setting.gradle 创建 Bundle
    遍历 Bundle 目录,如果查找到 setting.gradle 文件,就创建一下 Bundle 对象:

  1. public class Bundle implements Serializable, Comparable<Bundle> {

  2.    private String localPath;

  3.    private String name;//文件夹名称

  4.    private List<GradleModule> moduleList;//包含的module

  5.    private String packageId;

  6.    private String groupId;//groupId

  7.    private String artifactId;//artifactId

  8.    private Map<Bundle, Dependency> dependencyMap;//依赖关系表

  9.    ...

  10. }

  • 3.3 根据 build.gradle 创建 module
    创建好 bundle 之后,遍历 bundle 的子目录,查找 build.gradle 文件,然后创建 module 对象:

  1. public class GradleModule implements Serializable{

  2.    private String localPath;

  3.    private String name;//module的名称

  4.    private Bundle bundle;//隶属那个Bundle

  5.    private List<JavaFile> javaFileList;//module包含的import

  6.    ...

  7. }

  • 3.4 查找 Java 文件所在的 src 目录,创建 JavaFile
    查找 build.gradle 中的 src 属性,找到 Java 代码的存放位置,获取 *.java 文件的列表,创建 JavaFile 对象:

  1. public class JavaFile implements Serializable {

  2.    private String className;//类全称

  3.    private List<ImportModel> imports;//该类的imports文件

  4.    private GradleModule parentModule;//所在的Bundle

  5.    ...

  6. }    

  • 3.5 解析 Java 文件
    这一步是整个方案的核心,需要解析 Java 的语法,将 Java 文件的 import 区域过滤出来。

  • 3.6 整理 import 区域,删除多余的 import
    将 import 的类文件,在 Java 文件中进行搜索,如果未引用到,则删除该 import,如果存在,则保留。然后创建 import 对象:

  1. public class ImportModel implements Serializable {

  2.    private String className;//该import的包名+类名

  3.    private Bundle dependBundle;//该类所在的Bundle

  4.    ...

  5. }    

  • 3.7 建立映射关系表 Map
    经过上述递归算法,我们建立了 Bundle、Module、JavaFile、ImportModel 之间的树结构。并且保存了所有 Java 文件与其所属 Bundle 之间的映射关系。

  1. private Map<String, Bundle> mJavaFileBundleMap = new LinkedHashMap<>();//Java文件与所属Bundle之间的映射关系

  2. private List<JavaFile> mAllJavaFile = new ArrayList<>();//所有Java文件的List

  3. private List<Bundle> mBundleList = new ArrayList<>();//所有Bundle的List  

  • 3.8 依赖分析

  1.   /**

  2.     * 依赖分析

  3.     * mochuan.zhb@alibaba-inc.com

  4.     */

  5.    private void dependenciesAnalysis() {

  6.        for (JavaFile javaFile : mAllJavaFile) {//遍历所有的Java文件

  7.            //获取Java文件的Import区域列表

  8.            List<ImportModel> importModelList = javaFile.getImports();

  9.            //获取当前Java文件所在的Bundle

  10.            Bundle currentBundle = javaFile.getParentModule().getBundle();

  11.            //获取当前Bundle与其他Bundle依赖映射表

  12.            Map<Bundle, Dependency> dependencyMap = currentBundle.getDependencyMap();

  13.            if (dependencyMap == null) {

  14.                dependencyMap = new HashMap<>();

  15.                currentBundle.setDependencyMap(dependencyMap);

  16.            }

  17.            if (importModelList == null || importModelList.size() == 0) {

  18.                continue;

  19.            }

  20.            //遍历Import列表

  21.            for (ImportModel importModel : importModelList) {

  22.                String importClassName = importModel.getClassName();

  23.                if (isClassInWhiteList(importClassName)) {

  24.                    continue;

  25.                }

  26.                //查找import中类,所在的Bundle

  27.                Bundle bundle = mJavaFileBundleMap.get(importClassName);

  28.                if (bundle == null) {

  29.                    //没有查到该类所在的Bundle

  30.                    JDALog.info(String.format("%s depend bundle not found.", importModel.getClassName()));

  31.                } else if (bundle == javaFile.getParentModule().getBundle()) {

  32.                    //内部依赖;该import类和当前类在同一个Bundle中

  33.                    JDALog.info("internal depend.");

  34.                } else {

  35.                    //currentBundle依赖bundle

  36.                    Dependency dependency = dependencyMap.get(bundle);

  37.                    if (dependency == null) {

  38.                        dependency = new Dependency();

  39.                        dependencyMap.put(bundle, dependency);

  40.                    }

  41.                    //将依赖次数+1

  42.                    dependency.setDependCount(dependency.getDependCount() + 1);

  43.                    //能找到对应的Bundle依赖

  44.                    JDALog.info(String.format("%s depend %s", javaFile.getParentModule().getBundle().getName(), bundle.getName()));

  45.                }

  46.            }

  47.        }

  48.    }


3

依赖结果分析


我们把有相互依赖的 Bundle 进行连线,得到如下图:


化成圆形的图为:


为了更加准确地衡量 Bundle 之间的依赖程度,后续我们可以将依赖关系转换成 markdown 表格形式:更具体地展示 Bundle 之间依赖、以及被依赖的情况,以及被依赖多少次也能够清晰展现。除此之外,我们甚至可以知道具体是依赖哪个类。

4

总结


  • 上述分析方法有效:有了上述分析结果,为我们后续减小包大小、增删 Bundle、Bundle 升级提供了强有力的指导,为后续解除 Bundle 之间的依赖提供了详细的数据参考;

  • 从依赖表中,我们也可以看到哪些 Bundle 是叶子节点,可以根据是否叶子节点确定 packageId 的分配。

  • 对于通过反射的方式进行依赖的情况,目前还比较难统计到:

    比如Class.forName("com.koubei.android.xxx")之类的,后续可以考虑其他方案进行完善。

往期阅读


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存